home *** CD-ROM | disk | FTP | other *** search
/ Usenet 1993 July / InfoMagic USENET CD-ROM July 1993.ISO / sources / unix / volume23 / vixie-cron / part03 < prev    next >
Encoding:
Internet Message Format  |  1990-10-09  |  31.3 KB

  1. Subject:  v23i030:  A cron/crontab replacement, Part03/03
  2. Newsgroups: comp.sources.unix
  3. Approved: rsalz@uunet.UU.NET
  4. X-Checksum-Snefru: b389277b 0cbb344a 8f68432b 0ea68de7
  5.  
  6. Submitted-by: Paul A Vixie <vixie@vixie.sf.ca.us>
  7. Posting-number: Volume 23, Issue 30
  8. Archive-name: vixie-cron/part03
  9.  
  10. #! /bin/sh
  11. # This is a shell archive.  Remove anything before this line, then unpack
  12. # it by saving it into a file and typing "sh file".  To overwrite existing
  13. # files, type "sh file -c".  You can also feed this as standard input via
  14. # unshar, or by typing "sh <file", e.g..  If this archive is complete, you
  15. # will see the following message at the end:
  16. #        "End of archive 3 (of 3)."
  17. # Contents:  do_command.c misc.c
  18. # Wrapped by vixie@volition.pa.dec.com on Wed Jul 18 00:32:49 1990
  19. PATH=/bin:/usr/bin:/usr/ucb ; export PATH
  20. if test -f 'do_command.c' -a "${1}" != "-c" ; then 
  21.   echo shar: Will not clobber existing file \"'do_command.c'\"
  22. else
  23. echo shar: Extracting \"'do_command.c'\" \(14961 characters\)
  24. sed "s/^X//" >'do_command.c' <<'END_OF_FILE'
  25. X#if !defined(lint) && !defined(LINT)
  26. static char rcsid[] = "$Header: do_command.c,v 2.1 90/07/18 00:23:38 vixie Exp $";
  27. X#endif
  28. X
  29. X/* $Source: /jove_u3/vixie/src/cron/RCS/do_command.c,v $
  30. X * $Revision: 2.1 $
  31. X * $Log:    do_command.c,v $
  32. X * Revision 2.1  90/07/18  00:23:38  vixie
  33. X * Baseline for 4.4BSD release
  34. X * 
  35. X * Revision 2.0  88/12/10  04:57:44  vixie
  36. X * V2 Beta
  37. X * 
  38. X * Revision 1.5  88/11/29  13:06:06  vixie
  39. X * seems to work on Ultrix 3.0 FT1
  40. X * 
  41. X * Revision 1.4  87/05/02  17:33:35  paul
  42. X * baseline for mod.sources release
  43. X * 
  44. X * Revision 1.3  87/04/09  00:03:58  paul
  45. X * improved data hiding, locality of declaration/references
  46. X * fixed a rs@mirror bug by redesigning the mailto stuff completely
  47. X * 
  48. X * Revision 1.2  87/03/19  12:46:24  paul
  49. X * implemented suggestions from rs@mirror (Rich $alz):
  50. X *    MAILTO="" means no mail should be sent
  51. X *    various fixes of bugs or lint complaints
  52. X *    put a To: line in the mail message
  53. X * 
  54. X * Revision 1.1  87/01/26  23:47:00  paul
  55. X * Initial revision
  56. X */
  57. X
  58. X/* Copyright 1988,1990 by Paul Vixie
  59. X * All rights reserved
  60. X *
  61. X * Distribute freely, except: don't remove my name from the source or
  62. X * documentation (don't take credit for my work), mark your changes (don't
  63. X * get me blamed for your possible bugs), don't alter or remove this
  64. X * notice.  May be sold if buildable source is provided to buyer.  No
  65. X * warrantee of any kind, express or implied, is included with this
  66. X * software; use at your own risk, responsibility for damages (if any) to
  67. X * anyone resulting from the use of this software rests entirely with the
  68. X * user.
  69. X *
  70. X * Send bug reports, bug fixes, enhancements, requests, flames, etc., and
  71. X * I'll try to keep a version up to date.  I can be reached as follows:
  72. X * Paul Vixie, 329 Noe Street, San Francisco, CA, 94114, (415) 864-7013,
  73. X * paul@vixie.sf.ca.us || {hoptoad,pacbell,decwrl,crash}!vixie!paul
  74. X */
  75. X
  76. X
  77. X#include "cron.h"
  78. X#include <signal.h>
  79. X#include <pwd.h>
  80. X#if defined(BSD)
  81. X# include <sys/wait.h>
  82. X#endif /*BSD*/
  83. X#if defined(sequent)
  84. X# include <strings.h>
  85. X# include <sys/universe.h>
  86. X#endif
  87. X
  88. X
  89. void
  90. do_command(cmd, u)
  91. X    char    *cmd;
  92. X    user    *u;
  93. X{
  94. X    extern int    fork(), _exit();
  95. X    extern void    child_process(), log_it();
  96. X    extern char    *env_get();
  97. X
  98. X    Debug(DPROC, ("[%d] do_command(%s, (%s,%d,%d))\n",
  99. X        getpid(), cmd, env_get(USERENV, u->envp), u->uid, u->gid))
  100. X
  101. X    /* fork to become asynchronous -- parent process is done immediately,
  102. X     * and continues to run the normal cron code, which means return to
  103. X     * tick().  the child and grandchild don't leave this function, alive.
  104. X     *
  105. X     * vfork() is unsuitable, since we have much to do, and the parent
  106. X     * needs to be able to run off and fork other processes.
  107. X     */
  108. X    switch (fork())
  109. X    {
  110. X    case -1:
  111. X        log_it("CROND",getpid(),"error","can't fork");
  112. X        break;
  113. X    case 0:
  114. X        /* child process */
  115. X        child_process(cmd, u);
  116. X        Debug(DPROC, ("[%d] child process done, exiting\n", getpid()))
  117. X        _exit(OK_EXIT);
  118. X        break;
  119. X    }
  120. X    Debug(DPROC, ("[%d] main process returning to work\n", getpid()))
  121. X}
  122. X
  123. X
  124. static void
  125. child_process(cmd, u)
  126. X    char    *cmd;
  127. X    user    *u;
  128. X{
  129. X    extern struct passwd    *getpwnam();
  130. X    extern void    sigpipe_func(), be_different(), log_it();
  131. X    extern int    VFORK();
  132. X    extern char    *index(), *env_get();
  133. X
  134. X    auto int    stdin_pipe[2], stdout_pipe[2];
  135. X    register char    *input_data, *usernm, *mailto;
  136. X    auto int    children = 0;
  137. X#if defined(sequent)
  138. X    extern void    do_univ();
  139. X#endif
  140. X
  141. X    Debug(DPROC, ("[%d] child_process('%s')\n", getpid(), cmd))
  142. X
  143. X    /* mark ourselves as different to PS command watchers by upshifting
  144. X     * our program name.  This has no effect on some kernels.
  145. X     */
  146. X    {
  147. X        register char    *pch;
  148. X
  149. X        for (pch = ProgramName;  *pch;  pch++)
  150. X            *pch = MkUpper(*pch);
  151. X    }
  152. X
  153. X    /* discover some useful and important environment settings
  154. X     */
  155. X    usernm = env_get(USERENV, u->envp);
  156. X    mailto = env_get("MAILTO", u->envp);
  157. X
  158. X#if defined(BSD)
  159. X    /* our parent is watching for our death by catching SIGCHLD.  we
  160. X     * do not care to watch for our children's deaths this way -- we
  161. X     * use wait() explictly.  so we have to disable the signal (which
  162. X     * was inherited from the parent).
  163. X     *
  164. X     * this isn't needed for system V, since our parent is already
  165. X     * SIG_IGN on SIGCLD -- which, hopefully, will cause children to
  166. X     * simply vanish when they die.
  167. X     */
  168. X    (void) signal(SIGCHLD, SIG_IGN);
  169. X#endif /*BSD*/
  170. X
  171. X    /* create some pipes to talk to our future child
  172. X     */
  173. X    pipe(stdin_pipe);    /* child's stdin */
  174. X    pipe(stdout_pipe);    /* child's stdout */
  175. X    
  176. X    /* since we are a forked process, we can diddle the command string
  177. X     * we were passed -- nobody else is going to use it again, right?
  178. X     *
  179. X     * if a % is present in the command, previous characters are the
  180. X     * command, and subsequent characters are the additional input to
  181. X     * the command.  Subsequent %'s will be transformed into newlines,
  182. X     * but that happens later.
  183. X     */
  184. X    if (NULL == (input_data = index(cmd, '%')))
  185. X    {
  186. X        /* no %.  point input_data at a null string.
  187. X         */
  188. X        input_data = "";
  189. X    }
  190. X    else
  191. X    {
  192. X        /* % found.  replace with a null (remember, we're a forked
  193. X         * process and the string won't be reused), and increment
  194. X         * input_data to point at the following character.
  195. X         */
  196. X        *input_data++ = '\0';
  197. X    }
  198. X
  199. X    /* fork again, this time so we can exec the user's command.  Vfork()
  200. X     * is okay this time, since we are going to exec() pretty quickly.
  201. X     * I'm assuming that closing pipe ends &whatnot will not affect our
  202. X     * suspended pseudo-parent/alter-ego.
  203. X     */
  204. X    if (VFORK() == 0)
  205. X    {
  206. X        Debug(DPROC, ("[%d] grandchild process VFORK()'ed\n", getpid()))
  207. X
  208. X        /* write a log message.  we've waited this long to do it
  209. X         * because it was not until now that we knew the PID that
  210. X         * the actual user command shell was going to get and the
  211. X         * PID is part of the log message.
  212. X         */
  213. X#ifdef LOG_FILE
  214. X        {
  215. X            extern char *mkprints();
  216. X            char *x = mkprints(cmd, strlen(cmd));
  217. X
  218. X            log_it(usernm, getpid(), "CMD", x);
  219. X            free(x);
  220. X        }
  221. X#endif
  222. X
  223. X        /* get new pgrp, void tty, etc.
  224. X         */
  225. X        be_different();
  226. X
  227. X        /* close the pipe ends that we won't use.  this doesn't affect
  228. X         * the parent, who has to read and write them; it keeps the
  229. X         * kernel from recording us as a potential client TWICE --
  230. X         * which would keep it from sending SIGPIPE in otherwise
  231. X         * appropriate circumstances.
  232. X         */
  233. X        close(stdin_pipe[WRITE_PIPE]);
  234. X        close(stdout_pipe[READ_PIPE]);
  235. X
  236. X        /* grandchild process.  make std{in,out} be the ends of
  237. X         * pipes opened by our daddy; make stderr go to stdout.
  238. X         */
  239. X        close(STDIN);    dup2(stdin_pipe[READ_PIPE], STDIN);
  240. X        close(STDOUT);    dup2(stdout_pipe[WRITE_PIPE], STDOUT);
  241. X        close(STDERR);    dup2(STDOUT, STDERR);
  242. X
  243. X        /* close the pipes we just dup'ed.  The resources will remain,
  244. X         * since they've been dup'ed... :-)...
  245. X         */
  246. X        close(stdin_pipe[READ_PIPE]);
  247. X        close(stdout_pipe[WRITE_PIPE]);
  248. X
  249. X# if defined(sequent)
  250. X        /* set our login universe.  Do this in the grandchild
  251. X         * so that the child can invoke /usr/lib/sendmail
  252. X         * without surprises.
  253. X         */
  254. X        do_univ(u);
  255. X# endif
  256. X
  257. X        /* set our directory, uid and gid.  Set gid first, since once
  258. X         * we set uid, we've lost root privledges.  (oops!)
  259. X         */
  260. X        setgid(u->gid);
  261. X# if defined(BSD)
  262. X        initgroups(env_get(USERENV, u->envp), u->gid);
  263. X# endif
  264. X        setuid(u->uid);        /* you aren't root after this... */
  265. X        chdir(env_get("HOME", u->envp));
  266. X
  267. X        /* exec the command.
  268. X         */
  269. X        {
  270. X            char    *shell = env_get("SHELL", u->envp);
  271. X
  272. X# if DEBUGGING
  273. X            if (DebugFlags & DTEST) {
  274. X                fprintf(stderr,
  275. X                "debug DTEST is on, not exec'ing command.\n");
  276. X                fprintf(stderr,
  277. X                "\tcmd='%s' shell='%s'\n", cmd, shell);
  278. X                _exit(OK_EXIT);
  279. X            }
  280. X# endif /*DEBUGGING*/
  281. X            /* normally you can't put debugging stuff here because
  282. X             * it gets mailed with the command output.
  283. X             */
  284. X            /*
  285. X            Debug(DPROC, ("[%d] execle('%s', '%s', -c, '%s')\n",
  286. X                    getpid(), shell, shell, cmd))
  287. X             */
  288. X
  289. X# ifdef bad_idea
  290. X            /* files writable by non-owner are a no-no
  291. X             */
  292. X            {
  293. X                struct stat sb;
  294. X
  295. X                if (0 != stat(cmd, &sb)) {
  296. X                    fputs("crond: stat(2): ", stderr);
  297. X                    perror(cmd);
  298. X                    _exit(ERROR_EXIT);
  299. X                } else if (sb.st_mode & 022) {
  300. X                    fprintf(stderr,
  301. X                    "crond: %s writable by nonowner\n",
  302. X                        cmd);
  303. X                    _exit(ERROR_EXIT);
  304. X                } else if (sb.st_uid & 022) {
  305. X                    fprintf(stderr,
  306. X                    "crond: %s owned by uid %d\n",
  307. X                        cmd, sb.st_uid);
  308. X                    _exit(ERROR_EXIT);
  309. X                }
  310. X            }
  311. X# endif /*bad_idea*/
  312. X
  313. X            execle(shell, shell, "-c", cmd, (char *)0, u->envp);
  314. X            fprintf(stderr, "execl: couldn't exec `%s'\n", shell);
  315. X            perror("execl");
  316. X            _exit(ERROR_EXIT);
  317. X        }
  318. X    }
  319. X
  320. X    children++;
  321. X
  322. X    /* middle process, child of original cron, parent of process running
  323. X     * the user's command.
  324. X     */
  325. X
  326. X    Debug(DPROC, ("[%d] child continues, closing pipes\n", getpid()))
  327. X
  328. X    /* close the ends of the pipe that will only be referenced in the
  329. X     * grandchild process...
  330. X     */
  331. X    close(stdin_pipe[READ_PIPE]);
  332. X    close(stdout_pipe[WRITE_PIPE]);
  333. X
  334. X    /*
  335. X     * write, to the pipe connected to child's stdin, any input specified
  336. X     * after a % in the crontab entry.  while we copy, convert any
  337. X     * additional %'s to newlines.  when done, if some characters were
  338. X     * written and the last one wasn't a newline, write a newline.
  339. X     *
  340. X     * Note that if the input data won't fit into one pipe buffer (2K
  341. X     * or 4K on most BSD systems), and the child doesn't read its stdin,
  342. X     * we would block here.  the solution, of course, is to fork again.
  343. X     */
  344. X
  345. X    if (*input_data && fork() == 0) {
  346. X        register FILE    *out = fdopen(stdin_pipe[WRITE_PIPE], "w");
  347. X        register int    need_newline = FALSE;
  348. X        register int    escaped = FALSE;
  349. X        register int    ch;
  350. X
  351. X        Debug(DPROC, ("[%d] child2 sending data to grandchild\n", getpid()))
  352. X
  353. X        /* close the pipe we don't use, since we inherited it and
  354. X         * are part of its reference count now.
  355. X         */
  356. X        close(stdout_pipe[READ_PIPE]);
  357. X
  358. X        /* translation:
  359. X         *    \% -> %
  360. X         *    %  -> \n
  361. X         *    \x -> \x    for all x != %
  362. X         */
  363. X        while (ch = *input_data++)
  364. X        {
  365. X            if (escaped) {
  366. X                if (ch != '%')
  367. X                    putc('\\', out);
  368. X            } else {
  369. X                if (ch == '%')
  370. X                    ch = '\n';
  371. X            }
  372. X
  373. X            if (!(escaped = (ch == '\\'))) {
  374. X                putc(ch, out);
  375. X                need_newline = (ch != '\n');
  376. X            }
  377. X        }
  378. X        if (escaped)
  379. X            putc('\\', out);
  380. X        if (need_newline)
  381. X            putc('\n', out);
  382. X
  383. X        /* close the pipe, causing an EOF condition.  fclose causes
  384. X         * stdin_pipe[WRITE_PIPE] to be closed, too.
  385. X         */
  386. X        fclose(out);
  387. X
  388. X        Debug(DPROC, ("[%d] child2 done sending to grandchild\n", getpid()))
  389. X        exit(0);
  390. X    }
  391. X
  392. X    /* close the pipe to the grandkiddie's stdin, since its wicked uncle
  393. X     * ernie back there has it open and will close it when he's done.
  394. X     */
  395. X    close(stdin_pipe[WRITE_PIPE]);
  396. X
  397. X    children++;
  398. X
  399. X    /*
  400. X     * read output from the grandchild.  it's stderr has been redirected to
  401. X     * it's stdout, which has been redirected to our pipe.  if there is any
  402. X     * output, we'll be mailing it to the user whose crontab this is...
  403. X     * when the grandchild exits, we'll get EOF.
  404. X     */
  405. X
  406. X    Debug(DPROC, ("[%d] child reading output from grandchild\n", getpid()))
  407. X
  408. X    {
  409. X        register FILE    *in = fdopen(stdout_pipe[READ_PIPE], "r");
  410. X        register int    ch = getc(in);
  411. X
  412. X        if (ch != EOF)
  413. X        {
  414. X            register FILE    *mail;
  415. X            register int    bytes = 1;
  416. X            union wait    status;
  417. X
  418. X            Debug(DPROC|DEXT,
  419. X                ("[%d] got data (%x:%c) from grandchild\n",
  420. X                    getpid(), ch, ch))
  421. X
  422. X            /* get name of recipient.  this is MAILTO if set to a
  423. X             * valid local username; USER otherwise.
  424. X             */
  425. X            if (mailto)
  426. X            {
  427. X                /* MAILTO was present in the environment
  428. X                 */
  429. X                if (!*mailto)
  430. X                {
  431. X                    /* ... but it's empty. set to NULL
  432. X                     */
  433. X                    mailto = NULL;
  434. X                }
  435. X            }
  436. X            else
  437. X            {
  438. X                /* MAILTO not present, set to USER.
  439. X                 */
  440. X                mailto = usernm;
  441. X            }
  442. X        
  443. X            /* if we are supposed to be mailing, MAILTO will
  444. X             * be non-NULL.  only in this case should we set
  445. X             * up the mail command and subjects and stuff...
  446. X             */
  447. X
  448. X            if (mailto)
  449. X            {
  450. X                extern FILE    *popen();
  451. X                extern char    *sprintf(), *print_cmd();
  452. X                register char    **env;
  453. X                auto char    mailcmd[MAX_COMMAND];
  454. X                auto char    hostname[MAXHOSTNAMELEN];
  455. X
  456. X                (void) gethostname(hostname, MAXHOSTNAMELEN);
  457. X                (void) sprintf(mailcmd, MAILCMD, mailto);
  458. X                if (!(mail = popen(mailcmd, "w")))
  459. X                {
  460. X                    perror(MAILCMD);
  461. X                    (void) _exit(ERROR_EXIT);
  462. X                }
  463. X                fprintf(mail, "From: root (Cron Daemon)\n");
  464. X                fprintf(mail, "To: %s\n", mailto);
  465. X                fprintf(mail,
  466. X                "Subject: cron for %s@%s said this\n",
  467. X                    usernm, first_word(hostname, ".")
  468. X                );
  469. X                fprintf(mail, "Date: %s", ctime(&TargetTime));
  470. X                fprintf(mail, "X-Cron-Cmd: <%s>\n", cmd);
  471. X                for (env = u->envp;  *env;  env++)
  472. X                    fprintf(mail, "X-Cron-Env: <%s>\n",
  473. X                        *env);
  474. X                fprintf(mail, "\n");
  475. X
  476. X                /* this was the first char from the pipe
  477. X                 */
  478. X                putc(ch, mail);
  479. X            }
  480. X
  481. X            /* we have to read the input pipe no matter whether
  482. X             * we mail or not, but obviously we only write to
  483. X             * mail pipe if we ARE mailing.
  484. X             */
  485. X
  486. X            while (EOF != (ch = getc(in)))
  487. X            {
  488. X                bytes++;
  489. X                if (mailto)
  490. X                    putc(ch, mail);
  491. X            }
  492. X
  493. X            /* only close pipe if we opened it -- i.e., we're
  494. X             * mailing...
  495. X             */
  496. X
  497. X            if (mailto) {
  498. X                Debug(DPROC, ("[%d] closing pipe to mail\n",
  499. X                    getpid()))
  500. X                /* Note: the pclose will probably see
  501. X                 * the termination of the grandchild
  502. X                 * in addition to the mail process, since
  503. X                 * it (the grandchild) is likely to exit
  504. X                 * after closing its stdout.
  505. X                 */
  506. X                status.w_status = pclose(mail);
  507. X            }
  508. X
  509. X            /* if there was output and we could not mail it,
  510. X             * log the facts so the poor user can figure out
  511. X             * what's going on.
  512. X             */
  513. X            if (mailto && status.w_status) {
  514. X                char buf[MAX_TEMPSTR];
  515. X
  516. X                sprintf(buf,
  517. X            "mailed %d byte%s of output but got status 0x%04x\n",
  518. X                    bytes, (bytes==1)?"":"s",
  519. X                    status.w_status);
  520. X                log_it(usernm, getpid(), "MAIL", buf);
  521. X            }
  522. X
  523. X        } /*if data from grandchild*/
  524. X
  525. X        Debug(DPROC, ("[%d] got EOF from grandchild\n", getpid()))
  526. X
  527. X        fclose(in);    /* also closes stdout_pipe[READ_PIPE] */
  528. X    }
  529. X
  530. X#if defined(BSD)
  531. X    /* wait for children to die.
  532. X     */
  533. X    for (;  children > 0;  children--)
  534. X    {
  535. X        int        pid;
  536. X        union wait    waiter;
  537. X
  538. X        Debug(DPROC, ("[%d] waiting for grandchild #%d to finish\n",
  539. X            getpid(), children))
  540. X        pid = wait(&waiter);
  541. X        if (pid < OK) {
  542. X            Debug(DPROC, ("[%d] no more grandchildren--mail written?\n",
  543. X                getpid()))
  544. X            break;
  545. X        }
  546. X        Debug(DPROC, ("[%d] grandchild #%d finished, status=%04x",
  547. X            getpid(), pid, waiter.w_status))
  548. X        if (waiter.w_coredump)
  549. X            Debug(DPROC, (", dumped core"))
  550. X        Debug(DPROC, ("\n"))
  551. X    }
  552. X#endif /*BSD*/
  553. X}
  554. X
  555. X
  556. X#if defined(sequent)
  557. X/* Dynix (Sequent) hack to put the user associated with
  558. X * the passed user structure into the ATT universe if
  559. X * necessary.  We have to dig the gecos info out of
  560. X * the user's password entry to see if the magic
  561. X * "universe(att)" string is present.  If we do change
  562. X * the universe, also set "LOGNAME".
  563. X */
  564. X
  565. void
  566. do_univ(u)
  567. X    user    *u;
  568. X{
  569. X    struct    passwd    *p;
  570. X    char    *s;
  571. X    int    i;
  572. X    char    envstr[MAX_ENVSTR], **env_set();
  573. X
  574. X    p = getpwuid(u->uid);
  575. X    (void) endpwent();
  576. X
  577. X    if (p == NULL)
  578. X        return;
  579. X
  580. X    s = p->pw_gecos;
  581. X
  582. X    for (i = 0; i < 4; i++)
  583. X    {
  584. X        if ((s = index(s, ',')) == NULL)
  585. X            return;
  586. X        s++;
  587. X    }
  588. X    if (strcmp(s, "universe(att)"))
  589. X        return;
  590. X
  591. X    (void) sprintf(envstr, "LOGNAME=%s", p->pw_name);
  592. X    u->envp = env_set(u->envp, envstr);
  593. X
  594. X    (void) universe(U_ATT);
  595. X}
  596. X#endif
  597. END_OF_FILE
  598. if test 14961 -ne `wc -c <'do_command.c'`; then
  599.     echo shar: \"'do_command.c'\" unpacked with wrong size!
  600. fi
  601. # end of 'do_command.c'
  602. fi
  603. if test -f 'misc.c' -a "${1}" != "-c" ; then 
  604.   echo shar: Will not clobber existing file \"'misc.c'\"
  605. else
  606. echo shar: Extracting \"'misc.c'\" \(13932 characters\)
  607. sed "s/^X//" >'misc.c' <<'END_OF_FILE'
  608. X#if !defined(lint) && !defined(LINT)
  609. static char rcsid[] = "$Header: misc.c,v 2.1 90/07/18 00:24:33 vixie Exp $";
  610. X#endif
  611. X
  612. X/* vix 26jan87 [RCS has the rest of the log]
  613. X * vix 15jan87 [added TIOCNOTTY, thanks csg@pyramid]
  614. X * vix 30dec86 [written]
  615. X */
  616. X
  617. X/* Copyright 1988,1990 by Paul Vixie
  618. X * All rights reserved
  619. X *
  620. X * Distribute freely, except: don't remove my name from the source or
  621. X * documentation (don't take credit for my work), mark your changes (don't
  622. X * get me blamed for your possible bugs), don't alter or remove this
  623. X * notice.  May be sold if buildable source is provided to buyer.  No
  624. X * warrantee of any kind, express or implied, is included with this
  625. X * software; use at your own risk, responsibility for damages (if any) to
  626. X * anyone resulting from the use of this software rests entirely with the
  627. X * user.
  628. X *
  629. X * Send bug reports, bug fixes, enhancements, requests, flames, etc., and
  630. X * I'll try to keep a version up to date.  I can be reached as follows:
  631. X * Paul Vixie, 329 Noe Street, San Francisco, CA, 94114, (415) 864-7013,
  632. X * paul@vixie.sf.ca.us || {hoptoad,pacbell,decwrl,crash}!vixie!paul
  633. X */
  634. X
  635. X
  636. X#include "cron.h"
  637. X#include <sys/time.h>
  638. X#include <sys/resource.h>
  639. X#include <sys/ioctl.h>
  640. X#include <sys/file.h>
  641. X#include <errno.h>
  642. X#if defined(ATT)
  643. X# include <fcntl.h>
  644. X#endif
  645. X
  646. X
  647. void log_it(), be_different(), acquire_daemonlock();
  648. X
  649. X
  650. char *
  651. savestr(str)
  652. X    char    *str;
  653. X{
  654. X    extern    int    strlen();
  655. X    extern    char    *malloc(), *strcpy();
  656. X    /**/    char    *temp;
  657. X
  658. X    temp = malloc((unsigned) (strlen(str) + 1));
  659. X    (void) strcpy(temp, str);
  660. X    return temp;
  661. X}
  662. X
  663. X
  664. int
  665. nocase_strcmp(left, right)
  666. X    char    *left;
  667. X    char    *right;
  668. X{
  669. X    while (*left && (MkLower(*left) == MkLower(*right)))
  670. X    {
  671. X        left++;
  672. X        right++;
  673. X    }
  674. X    return MkLower(*left) - MkLower(*right);
  675. X}
  676. X
  677. X
  678. int
  679. strcmp_until(left, right, until)
  680. X    char    *left;
  681. X    char    *right;
  682. X    char    until;
  683. X{
  684. X    register int    diff;
  685. X
  686. X    Debug(DPARS|DEXT, ("strcmp_until(%s,%s,%c) ... ", left, right, until))
  687. X
  688. X    while (*left && *left != until && *left == *right)
  689. X    {
  690. X        left++;
  691. X        right++;
  692. X    }
  693. X
  694. X    if (    (*left=='\0' || *left == until) 
  695. X        &&    (*right=='\0' || *right == until)
  696. X       )
  697. X        diff = 0;
  698. X    else
  699. X        diff = *left - *right;
  700. X
  701. X    Debug(DPARS|DEXT, ("%d\n", diff))
  702. X
  703. X    return diff;
  704. X}
  705. X
  706. X
  707. X/* strdtb(s) - delete trailing blanks in string 's' and return new length
  708. X */
  709. int
  710. strdtb(s)
  711. X    register char    *s;
  712. X{
  713. X    register char    *x = s;
  714. X
  715. X    /* scan forward to the null
  716. X     */
  717. X    while (*x)
  718. X        x++;
  719. X
  720. X    /* scan backward to either the first character before the string,
  721. X     * or the last non-blank in the string, whichever comes first.
  722. X     */
  723. X    do    {x--;}
  724. X    while (x >= s && isspace(*x));
  725. X
  726. X    /* one character beyond where we stopped above is where the null
  727. X     * goes.
  728. X     */
  729. X    *++x = '\0';
  730. X
  731. X    /* the difference between the position of the null character and
  732. X     * the position of the first character of the string is the length.
  733. X     */
  734. X    return x - s;
  735. X}
  736. X
  737. X
  738. int
  739. set_debug_flags(flags)
  740. X    char    *flags;
  741. X{
  742. X    /* debug flags are of the form    flag[,flag ...]
  743. X     *
  744. X     * if an error occurs, print a message to stdout and return FALSE.
  745. X     * otherwise return TRUE after setting ERROR_FLAGS.
  746. X     */
  747. X
  748. X#if !DEBUGGING
  749. X
  750. X    printf("this program was compiled without debugging enabled\n");
  751. X    return FALSE;
  752. X
  753. X#else /* DEBUGGING */
  754. X
  755. X    char    *pc = flags;
  756. X
  757. X    DebugFlags = 0;
  758. X
  759. X    while (*pc)
  760. X    {
  761. X        char    **test;
  762. X        int    mask;
  763. X
  764. X        /* try to find debug flag name in our list.
  765. X         */
  766. X        for (    test = DebugFlagNames, mask = 1;
  767. X            *test && strcmp_until(*test, pc, ',');
  768. X            test++, mask <<= 1
  769. X            )
  770. X            ;
  771. X
  772. X        if (!*test)
  773. X        {
  774. X            fprintf(stderr,
  775. X                "unrecognized debug flag <%s> <%s>\n",
  776. X                flags, pc);
  777. X            return FALSE;
  778. X        }
  779. X
  780. X        DebugFlags |= mask;
  781. X
  782. X        /* skip to the next flag
  783. X         */
  784. X        while (*pc && *pc != ',')
  785. X            pc++;
  786. X        if (*pc == ',')
  787. X            pc++;
  788. X    }
  789. X
  790. X    if (DebugFlags)
  791. X    {
  792. X        int    flag;
  793. X
  794. X        fprintf(stderr, "debug flags enabled:");
  795. X
  796. X        for (flag = 0;  DebugFlagNames[flag];  flag++)
  797. X            if (DebugFlags & (1 << flag))
  798. X                fprintf(stderr, " %s", DebugFlagNames[flag]);
  799. X        fprintf(stderr, "\n");
  800. X    }
  801. X
  802. X    return TRUE;
  803. X
  804. X#endif /* DEBUGGING */
  805. X}
  806. X
  807. X
  808. X#if defined(BSD)
  809. void
  810. set_cron_uid()
  811. X{
  812. X    int    seteuid();
  813. X
  814. X    if (seteuid(ROOT_UID) < OK)
  815. X    {
  816. X        perror("seteuid");
  817. X        exit(ERROR_EXIT);
  818. X    }
  819. X}
  820. X#endif
  821. X
  822. X#if defined(ATT)
  823. void
  824. set_cron_uid()
  825. X{
  826. X    int    setuid();
  827. X
  828. X    if (setuid(ROOT_UID) < OK)
  829. X    {
  830. X        perror("setuid");
  831. X        exit(ERROR_EXIT);
  832. X    }
  833. X}
  834. X#endif
  835. X
  836. void
  837. set_cron_cwd()
  838. X{
  839. X    extern int    errno;
  840. X    struct stat    sb;
  841. X
  842. X    /* first check for CRONDIR ("/var/cron" or some such)
  843. X     */
  844. X    if (stat(CRONDIR, &sb) < OK && errno == ENOENT) {
  845. X        perror(CRONDIR);
  846. X        if (OK == mkdir(CRONDIR, 0700)) {
  847. X            fprintf(stderr, "%s: created\n", CRONDIR);
  848. X            stat(CRONDIR, &sb);
  849. X        } else {
  850. X            fprintf(stderr, "%s: ", CRONDIR);
  851. X            perror("mkdir");
  852. X            exit(ERROR_EXIT);
  853. X        }
  854. X    }
  855. X    if (!(sb.st_mode & S_IFDIR)) {
  856. X        fprintf(stderr, "'%s' is not a directory, bailing out.\n",
  857. X            CRONDIR);
  858. X        exit(ERROR_EXIT);
  859. X    }
  860. X    if (chdir(CRONDIR) < OK) {
  861. X        fprintf(stderr, "cannot chdir(%s), bailing out.\n", CRONDIR);
  862. X        perror(CRONDIR);
  863. X        exit(ERROR_EXIT);
  864. X    }
  865. X
  866. X    /* CRONDIR okay (now==CWD), now look at SPOOL_DIR ("tabs" or some such)
  867. X     */
  868. X    if (stat(SPOOL_DIR, &sb) < OK && errno == ENOENT) {
  869. X        perror(SPOOL_DIR);
  870. X        if (OK == mkdir(SPOOL_DIR, 0700)) {
  871. X            fprintf(stderr, "%s: created\n", SPOOL_DIR);
  872. X            stat(SPOOL_DIR, &sb);
  873. X        } else {
  874. X            fprintf(stderr, "%s: ", SPOOL_DIR);
  875. X            perror("mkdir");
  876. X            exit(ERROR_EXIT);
  877. X        }
  878. X    }
  879. X    if (!(sb.st_mode & S_IFDIR)) {
  880. X        fprintf(stderr, "'%s' is not a directory, bailing out.\n",
  881. X            SPOOL_DIR);
  882. X        exit(ERROR_EXIT);
  883. X    }
  884. X}
  885. X
  886. X
  887. X#if defined(BSD)
  888. void
  889. be_different()
  890. X{
  891. X    /* release the control terminal:
  892. X     *  get new pgrp (name after our PID)
  893. X     *  do an IOCTL to void tty association
  894. X     */
  895. X
  896. X    extern int    getpid(), setpgrp(), open(), ioctl(), close();
  897. X    auto int    fd;
  898. X
  899. X    (void) setpgrp(0, getpid());
  900. X
  901. X    if ((fd = open("/dev/tty", 2)) >= 0)
  902. X    {
  903. X        (void) ioctl(fd, TIOCNOTTY, (char*)0);
  904. X        (void) close(fd);
  905. X    }
  906. X}
  907. X#endif /*BSD*/
  908. X
  909. X#if defined(ATT)
  910. void
  911. be_different()
  912. X{
  913. X    /* not being a system V wiz, I don't know if this is what you have
  914. X     * to do to release your control terminal.  what I want to accomplish
  915. X     * is to keep this process from getting any signals from the tty.
  916. X     *
  917. X     * some system V person should let me know if this works... (vixie)
  918. X     */
  919. X    int    setpgrp(), close(), open();
  920. X
  921. X    (void) setpgrp();
  922. X
  923. X    (void) close(STDIN);    (void) open("/dev/null", 0);
  924. X    (void) close(STDOUT);    (void) open("/dev/null", 1);
  925. X    (void) close(STDERR);    (void) open("/dev/null", 2);
  926. X}
  927. X#endif /*ATT*/
  928. X
  929. X
  930. X/* acquire_daemonlock() - write our PID into /etc/crond.pid, unless
  931. X *    another daemon is already running, which we detect here.
  932. X */
  933. void
  934. acquire_daemonlock()
  935. X{
  936. X    int    fd = open(PIDFILE, O_RDWR|O_CREAT, 0644);
  937. X    FILE    *fp = fdopen(fd, "r+");
  938. X    int    pid = getpid(), otherpid;
  939. X    char    buf[MAX_TEMPSTR];
  940. X
  941. X    if (fd < 0 || fp == NULL) {
  942. X        sprintf(buf, "can't open or create %s, errno %d", PIDFILE, errno);
  943. X        log_it("CROND", pid, "DEATH", buf);
  944. X        exit(ERROR_EXIT);
  945. X    }
  946. X
  947. X    if (flock(fd, LOCK_EX|LOCK_NB) < OK) {
  948. X        int save_errno = errno;
  949. X
  950. X        fscanf(fp, "%d", &otherpid);
  951. X        sprintf(buf, "can't lock %s, otherpid may be %d, errno %d",
  952. X            PIDFILE, otherpid, save_errno);
  953. X        log_it("CROND", pid, "DEATH", buf);
  954. X        exit(ERROR_EXIT);
  955. X    }
  956. X
  957. X    rewind(fp);
  958. X    fprintf(fp, "%d\n", pid);
  959. X    fflush(fp);
  960. X    ftruncate(fd, ftell(fp));
  961. X
  962. X    /* abandon fd and fp even though the file is open. we need to
  963. X     * keep it open and locked, but we don't need the handles elsewhere.
  964. X     */
  965. X}
  966. X
  967. X/* get_char(file) : like getc() but increment LineNumber on newlines
  968. X */
  969. int
  970. get_char(file)
  971. X    FILE    *file;
  972. X{
  973. X    int    ch;
  974. X
  975. X    ch = getc(file);
  976. X    if (ch == '\n')
  977. X        Set_LineNum(LineNumber + 1)
  978. X    return ch;
  979. X}
  980. X
  981. X
  982. X/* unget_char(ch, file) : like ungetc but do LineNumber processing
  983. X */
  984. void
  985. unget_char(ch, file)
  986. X    int    ch;
  987. X    FILE    *file;
  988. X{
  989. X    ungetc(ch, file);
  990. X    if (ch == '\n')
  991. X        Set_LineNum(LineNumber - 1)
  992. X}
  993. X
  994. X
  995. X/* get_string(str, max, file, termstr) : like fgets() but
  996. X *        (1) has terminator string which should include \n
  997. X *        (2) will always leave room for the null
  998. X *        (3) uses get_char() so LineNumber will be accurate
  999. X *        (4) returns EOF or terminating character, whichever
  1000. X */
  1001. int
  1002. get_string(string, size, file, terms)
  1003. X    char    *string;
  1004. X    int    size;
  1005. X    FILE    *file;
  1006. X    char    *terms;
  1007. X{
  1008. X    int    ch;
  1009. X    char    *index();
  1010. X
  1011. X    while (EOF != (ch = get_char(file)) && !index(terms, ch))
  1012. X        if (size > 1)
  1013. X        {
  1014. X            *string++ = (char) ch;
  1015. X            size--;
  1016. X        }
  1017. X
  1018. X    if (size > 0)
  1019. X        *string = '\0';
  1020. X
  1021. X    return ch;
  1022. X}
  1023. X
  1024. X
  1025. X/* skip_comments(file) : read past comment (if any)
  1026. X */
  1027. void
  1028. skip_comments(file)
  1029. X    FILE    *file;
  1030. X{
  1031. X    int    ch;
  1032. X
  1033. X    while (EOF != (ch = get_char(file)))
  1034. X    {
  1035. X        /* ch is now the first character of a line.
  1036. X         */
  1037. X
  1038. X        while (ch == ' ' || ch == '\t')
  1039. X            ch = get_char(file);
  1040. X
  1041. X        if (ch == EOF)
  1042. X            break;
  1043. X
  1044. X        /* ch is now the first non-blank character of a line.
  1045. X         */
  1046. X
  1047. X        if (ch != '\n' && ch != '#')
  1048. X            break;
  1049. X
  1050. X        /* ch must be a newline or comment as first non-blank
  1051. X         * character on a line.
  1052. X         */
  1053. X
  1054. X        while (ch != '\n' && ch != EOF)
  1055. X            ch = get_char(file);
  1056. X
  1057. X        /* ch is now the newline of a line which we're going to
  1058. X         * ignore.
  1059. X         */
  1060. X    }
  1061. X    unget_char(ch, file);
  1062. X}
  1063. X
  1064. X/* int in_file(char *string, FILE *file)
  1065. X *    return TRUE if one of the lines in file matches string exactly,
  1066. X *    FALSE otherwise.
  1067. X */
  1068. int
  1069. in_file(string, file)
  1070. X    char *string;
  1071. X    FILE *file;
  1072. X{
  1073. X    char line[MAX_TEMPSTR];
  1074. X
  1075. X    /* let's be persnickety today.
  1076. X     */
  1077. X    if (!file) {
  1078. X        if (!string)
  1079. X            string = "0x0";
  1080. X        fprintf(stderr,
  1081. X            "in_file(\"%s\", 0x%x): called with NULL file--botch",
  1082. X            string, file);
  1083. X        exit(ERROR_EXIT);
  1084. X    }
  1085. X
  1086. X    rewind(file);
  1087. X    while (fgets(line, MAX_TEMPSTR, file)) {
  1088. X        if (line[0] != '\0')
  1089. X            line[strlen(line)-1] = '\0';
  1090. X        if (0 == strcmp(line, string))
  1091. X            return TRUE;
  1092. X    }
  1093. X    return FALSE;
  1094. X}
  1095. X
  1096. X
  1097. X/* int allowed(char *username)
  1098. X *    returns TRUE if (ALLOW_FILE exists and user is listed)
  1099. X *    or (DENY_FILE exists and user is NOT listed)
  1100. X *    or (neither file exists but user=="root" so it's okay)
  1101. X */
  1102. int
  1103. allowed(username)
  1104. X    char *username;
  1105. X{
  1106. X    static int    init = FALSE;
  1107. X    static FILE    *allow, *deny;
  1108. X
  1109. X    if (!init) {
  1110. X        init = TRUE;
  1111. X#if defined(ALLOW_FILE) && defined(DENY_FILE)
  1112. X        allow = fopen(ALLOW_FILE, "r");
  1113. X        deny = fopen(DENY_FILE, "r");
  1114. X        Debug(DMISC, ("allow/deny enabled, %d/%d\n", !!allow, !!deny))
  1115. X#else
  1116. X        allow = NULL;
  1117. X        deny = NULL;
  1118. X#endif
  1119. X    }
  1120. X
  1121. X    if (allow)
  1122. X        return (in_file(username, allow));
  1123. X    if (deny)
  1124. X        return (!in_file(username, deny));
  1125. X
  1126. X#if defined(ALLOW_ONLY_ROOT)
  1127. X    return (strcmp(username, ROOT_USER) == 0);
  1128. X#else
  1129. X    return TRUE;
  1130. X#endif
  1131. X}
  1132. X
  1133. X
  1134. X#if defined(LOG_FILE) || defined(SYSLOG)
  1135. void
  1136. log_it(username, pid, event, detail)
  1137. X    char    *username;
  1138. X    int    pid;
  1139. X    char    *event;
  1140. X    char    *detail;
  1141. X{
  1142. X#if defined(LOG_FILE)
  1143. X    extern struct tm    *localtime();
  1144. X    extern long        time();
  1145. X    extern char        *malloc();
  1146. X    auto char        *msg;
  1147. X    auto long        now = time((long *) 0);
  1148. X    register struct tm    *t = localtime(&now);
  1149. X    static int        log_fd = -1;
  1150. X#endif /*LOG_FILE*/
  1151. X
  1152. X#if defined(SYSLOG)
  1153. X    static int        syslog_open = 0;
  1154. X#endif
  1155. X
  1156. X
  1157. X#if defined(LOG_FILE)
  1158. X    /* we assume that MAX_TEMPSTR will hold the date, time, &punctuation.
  1159. X     */
  1160. X    msg = malloc(strlen(username)
  1161. X          + strlen(event)
  1162. X          + strlen(detail)
  1163. X          + MAX_TEMPSTR);
  1164. X
  1165. X    if (log_fd < OK) {
  1166. X        log_fd = open(LOG_FILE, O_WRONLY|O_APPEND|O_CREAT, 0600);
  1167. X        if (log_fd < OK) {
  1168. X            fprintf(stderr, "%s: can't open log file\n", ProgramName);
  1169. X            perror(LOG_FILE);
  1170. X        }
  1171. X    }
  1172. X
  1173. X    /* we have to sprintf() it because fprintf() doesn't always write
  1174. X     * everything out in one chunk and this has to be atomically appended
  1175. X     * to the log file.
  1176. X     */
  1177. X    sprintf(msg, "%s (%02d/%02d-%02d:%02d:%02d-%d) %s (%s)\n",
  1178. X        username,
  1179. X        t->tm_mon+1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, pid,
  1180. X        event, detail);
  1181. X
  1182. X    /* we have to run strlen() because sprintf() returns (char*) on BSD
  1183. X     */
  1184. X    if (log_fd < OK || write(log_fd, msg, strlen(msg)) < OK) {
  1185. X        fprintf(stderr, "%s: can't write to log file\n", ProgramName);
  1186. X        if (log_fd >= OK)
  1187. X            perror(LOG_FILE);
  1188. X        write(STDERR, msg, strlen(msg));
  1189. X    }
  1190. X
  1191. X    /* I suppose we could use alloca()...
  1192. X     */
  1193. X    free(msg);
  1194. X#endif /*LOG_FILE*/
  1195. X
  1196. X#if defined(SYSLOG)
  1197. X    if (!syslog_open) {
  1198. X        /* we don't use LOG_PID since the pid passed to us by
  1199. X         * our client may not be our own.  therefore we want to
  1200. X         * print the pid ourselves.
  1201. X         */
  1202. X# ifdef LOG_CRON
  1203. X        openlog(ProgramName, 0, LOG_CRON);
  1204. X# else
  1205. X# ifdef LOG_DAEMON
  1206. X        openlog(ProgramName, 0, LOG_DAEMON);
  1207. X# else
  1208. X        openlog(ProgramName, 0);
  1209. X# endif /*LOG_DAEMON*/
  1210. X# endif /*LOG_CRON*/
  1211. X        syslog_open = TRUE;        /* assume openlog success */
  1212. X    }
  1213. X
  1214. X    syslog(LOG_INFO, "(%s %d) %s (%s)\n",
  1215. X        username, pid, event, detail);
  1216. X
  1217. X#endif /*SYSLOG*/
  1218. X
  1219. X    if (DebugFlags) {
  1220. X        fprintf(stderr, "log_it: (%s %d) %s (%s)",
  1221. X            username, pid, event, detail);
  1222. X    }
  1223. X}
  1224. X#endif /*LOG_FILE || SYSLOG */
  1225. X
  1226. X
  1227. X/* two warnings:
  1228. X *    (1) this routine is fairly slow
  1229. X *    (2) it returns a pointer to static storage
  1230. X */
  1231. char *
  1232. first_word(s, t)
  1233. X    local char *s;    /* string we want the first word of */
  1234. X    local char *t;    /* terminators, implicitly including \0 */
  1235. X{
  1236. X    static char retbuf[2][MAX_TEMPSTR + 1];    /* sure wish I had GC */
  1237. X    static int retsel = 0;
  1238. X    local char *rb, *rp;
  1239. X    extern char *index();
  1240. X
  1241. X    /* select a return buffer */
  1242. X    retsel = 1-retsel;
  1243. X    rb = &retbuf[retsel][0];
  1244. X    rp = rb;
  1245. X
  1246. X    /* skip any leading terminators */
  1247. X    while (*s && (NULL != index(t, *s))) {s++;}
  1248. X
  1249. X    /* copy until next terminator or full buffer */
  1250. X    while (*s && (NULL == index(t, *s)) && (rp < &rb[MAX_TEMPSTR])) {
  1251. X        *rp++ = *s++;
  1252. X    }
  1253. X
  1254. X    /* finish the return-string and return it */
  1255. X    *rp = '\0';
  1256. X    return rb;
  1257. X}
  1258. X
  1259. X
  1260. X/* warning:
  1261. X *    heavily ascii-dependent.
  1262. X */
  1263. X
  1264. void
  1265. mkprint(dst, src, len)
  1266. X    register char *dst;
  1267. X    register unsigned char *src;
  1268. X    register int len;
  1269. X{
  1270. X    while (len-- > 0)
  1271. X    {
  1272. X        register unsigned char ch = *src++;
  1273. X
  1274. X        if (ch < ' ') {            /* control character */
  1275. X            *dst++ = '^';
  1276. X            *dst++ = ch + '@';
  1277. X        } else if (ch < 0177) {        /* printable */
  1278. X            *dst++ = ch;
  1279. X        } else if (ch == 0177) {    /* delete/rubout */
  1280. X            *dst++ = '^';
  1281. X            *dst++ = '?';
  1282. X        } else {            /* parity character */
  1283. X            sprintf(dst, "\\%03o", ch);
  1284. X            dst += 4;
  1285. X        }
  1286. X    }
  1287. X    *dst = NULL;
  1288. X}
  1289. X
  1290. X
  1291. X/* warning:
  1292. X *    returns a pointer to malloc'd storage, you must call free yourself.
  1293. X */
  1294. X
  1295. char *
  1296. mkprints(src, len)
  1297. X    register unsigned char *src;
  1298. X    register unsigned int len;
  1299. X{
  1300. X    extern char *malloc();
  1301. X    register char *dst = malloc(len*4 + 1);
  1302. X
  1303. X    mkprint(dst, src, len);
  1304. X
  1305. X    return dst;
  1306. X}
  1307. END_OF_FILE
  1308. if test 13932 -ne `wc -c <'misc.c'`; then
  1309.     echo shar: \"'misc.c'\" unpacked with wrong size!
  1310. fi
  1311. # end of 'misc.c'
  1312. fi
  1313. echo shar: End of archive 3 \(of 3\).
  1314. cp /dev/null ark3isdone
  1315. MISSING=""
  1316. for I in 1 2 3 ; do
  1317.     if test ! -f ark${I}isdone ; then
  1318.     MISSING="${MISSING} ${I}"
  1319.     fi
  1320. done
  1321. if test "${MISSING}" = "" ; then
  1322.     echo You have unpacked all 3 archives.
  1323.     rm -f ark[1-9]isdone
  1324. else
  1325.     echo You still need to unpack the following archives:
  1326.     echo "        " ${MISSING}
  1327. fi
  1328. ##  End of shell archive.
  1329. exit 0
  1330.  
  1331. exit 0 # Just in case...
  1332.